package org.sinekartads.core.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SignatureException;
import java.security.cert.CertificateException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.commons.lang3.StringUtils;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.signature.XMLSignature;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.tsp.TimeStampToken;
import org.sinekartads.model.domain.DigestInfo;
import org.sinekartads.model.domain.SecurityLevel.VerifyResult;
import org.sinekartads.model.domain.SignDisposition;
import org.sinekartads.model.domain.SignatureType.SignCategory;
import org.sinekartads.model.domain.TimeStampInfo;
import org.sinekartads.model.domain.Transitions.ChainSignature;
import org.sinekartads.model.domain.Transitions.DigestSignature;
import org.sinekartads.model.domain.Transitions.FinalizedSignature;
import org.sinekartads.model.domain.Transitions.MarkedSignature;
import org.sinekartads.model.domain.Transitions.SignedSignature;
import org.sinekartads.model.domain.TsRequestInfo;
import org.sinekartads.model.domain.TsResponseInfo;
import org.sinekartads.model.domain.VerifyInfo;
import org.sinekartads.model.domain.XMLSignatureInfo;
import org.sinekartads.model.oid.DigestAlgorithm;
import org.sinekartads.util.TemplateUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import xades4j.algorithms.Algorithm;
import xades4j.algorithms.ExclusiveCanonicalXMLWithoutComments;
import xades4j.algorithms.XPath2FilterTransform.XPath2Filter;
import xades4j.production.DataObjectReference;
import xades4j.production.ExtPropertiesDataGenerationContext;
import xades4j.production.SignatureAppendingStrategies;
import xades4j.production.SignedDataObjects;
import xades4j.production.XadesExtBesSigningProfile;
import xades4j.production.XadesExtTSigningProfile;
import xades4j.production.XadesSignatureResult;
import xades4j.production.XadesSignerExt;
import xades4j.production.XadesSigningProfile;
import xades4j.properties.AllDataObjsCommitmentTypeProperty;
import xades4j.properties.DataObjectDesc;
import xades4j.providers.TimeStampTokenGenerationException;
import xades4j.providers.TimeStampTokenProvider;
import xades4j.providers.impl.DefaultAlgorithmsProviderEx;
import xades4j.providers.impl.ExtKeyringDataProvider;
import xades4j.providers.impl.ExtSignaturePropertiesProvider;
import xades4j.utils.DOMHelper;
import xades4j.xml.sign.DOMUtils;
import xades4j.xml.sign.DigesterOutputStream;
import xades4j.xml.sign.ExtXMLSignature;

/**
* <p>
 * Implementation of SignatureService protocol for the XAdES signature.<br/>
 * The XML signature generated by this service apply the ENVELOPED disposition: the signed xml
 * file will then be equivalent to the original file, with the addition of the ds:Signature tag.
 * The eventual timeStamp will appear as an unsigned property of the signature.
 * In order to grant that the digest obtained by the preSign phase will match with the envelope
 * created by the postSign, the SignatureDTO must carry the signingTime from the first step to the 
 * second one and the signatureId used as base for the tag ids that will appear into the document. 
 * </p>
 * <br>
 * <h3>XAdES signature architecture</h3>
 * <p>
 * The XMLSignatureService uses a patched version of xades4j optimized for the Italian requirements
 * for the digital signature and for receiving externally evaluated signature. Moreover, it uses  
 * the version of BouncyCastle for jdk16.
 *    
 * </p><p>
 * The architecture generated for the SineKarta development is made up by the following classes:
 * <ul>
 * <li>{@link DomUtils}, a convenience suite of utility methods relative to the dom parsing and
 * manipulation
 * <li>{@link ExtXMLSignature}, a modified version of XMLSignature which allows to store the digest
 * that is being evaluated during the preSign and introduce the externally evaluated digital 
 * signature during the postSign  
 * <li>{@link XadesSignerExt}, an interface shared by the modified xades signers in order to allow
 * the XMLSignatureService to call the digest generation and exchange signingTime and signatureId
 * with the SignatureDTO object
 * <li>{@link SignerExtBES}, a modified version of SignerBES able to switch from XMLSignature to
 * ExtXMLSignature during the signature process
 * <li>{@link SignerExtT}, an extension of SignerExtBES for the XAdES-T signature implementation
 * <li>{@link ExtTimeStampTokenProvider} subclass of CMSSignatureService that uses {@link TimeStampService}
 * to contact the TimeStamp Authority
 * <li>{@link XadesExtBesSigningProfile} and {@link XadesExtTSigningProfile}, in accordance with the
 * xades4j standard, are the signing profiles used to generate {@link SignerExtBES} and {@link SignerExtT}
 * instances. 
 * <li>{@link ExtKeyringDataProvider}, used to simulate the digital signature application by xades4j with
 * throw-away private keys, in order to be able to store the resulting digest
 * <li>{@link ExtSignaturePropertiesProvider}, properties provider able to save and successively replace
 * the signing time value
 * <li>{@link ExtPropertiesDataGenerationContext}, an alteration of PropertiesDataGenerationContext using
 * ExtXMLSignature instead of XMLSignature during the digital signature evaluation
 * <li>{@link DigesterOutputStream}, stream used by ExtXMLSignature during the preSign phase to evaluate
 * and save the digest  
 * </ul>
 * </p><p>
 * The SineKarta development required moreover to perform the following bugfixes on the original xades4j code.
 * <ul>
 * <li>{@link DataGenBaseCertRefs}: corrected the certificate's IssuerDN extraction since the previous usage of
 * getIsssuerX500Principal() did not import all the attributes properly
 * <li>{@link ToXmlBaseTimeStampConverter}: added the "DER" encoding value to the attribute EncapsulatedTimeStamp
 * <li>{@link DefaultAlgorithmsProviderEx}: SHA256 set as default algorithm for the timestamp
 * </ul>
 * </p>
 * <h3>XAdES signature process</h3>
 * <p>
 * Depending by the application of a timeStamp or not, the CMSSignatureService will create a SignerExtT or a 
 * SignerExtBES with the relative signing profile and it injects the certificate chain. Since SignerExtT only 
 * overrides some methods of SignerExtBES in order to add the timeStamp to the unsigned properties, the whole
 * signature process will be carried out by SignerExtBES.
 * After that creates a reference to the xml code to be signed and build the signature envelope with the signer. 
 * The signature process follows the same steps of the original xades4j procedure, therefore will be explained only
 * the modifications that have been applied for SineKarta.
 * </p><p>
 * During the preSign phase, SignerExtBES generates and stores the signatureId, this value will be the externally 
 * set in the next phase. It prepare the XMLSignature normally and adds the certificate chain to the KeyInfo.
 * Then it proceeds generating the signatureProperties using the ExtSignaturePropertiesProvider and stores the 
 * signing time. 
 * It replaces then the XMLSignature with an equivalent ExtXMLSignature with a conversion to xml and a successive 
 * unmarshalling and the digest is evaluated by ExtXMLSignature.
 * </p><p>
 * During this step the digests for each reference of the SignerInfo are generated and when is required to sign
 * the SignerInfo, it evaluate with DigesterOutputStream the global digest that will be returned to the service. 
 * Having used ExtKeyringDataProvider allows then to simulate the signature with a throw-away private key.
 * </p><p>
 * The postSign phase set the properties and the digest that have been stored before and follows the same steps
 * of the preSign. Now the ExtXMLSignature receives the digital signature and is asked to sign the xml. The 
 * SignedInfo is signed again, but this time the resulting value is replaced with the externally evaluated signature.
 * After that, the ExtXMLSignature needs to receive the unsignedAttributes and is converted to XMLSignature
 * to continue with the normal xades4j execution.
 * </p>
 * @author adeprato
 */
public class XMLSignatureService 
		extends AbstractSignatureService  <	SignCategory,
											SignDisposition.XML,
											XMLSignatureInfo > {

	/**
	 * Provider for the timeStamp tokens. It receives the timestamp request by XMLSignatureService
	 * during the initialization and is added as the timeStampTokenProfile of XadesExtTSigningProfile.
	 * It will then be used during the unsigned properties generation. The provider obtains the token 
	 * from the TimeStamp Authority by means of TimeStampService and return it nested into a TimeStampTokenRes
	 * instance.
	 * @author amommo
	 */
	public static class ExtTimeStampTokenProvider implements TimeStampTokenProvider
	{
	    private TsRequestInfo tsRequest;
	    private TsResponseInfo tsResponse;
	    private TimeStampInfo timeStamp;

	    @Override
	    public final TimeStampTokenRes getTimeStampToken ( byte[] tsDigestInput,
	            										   String digestAlgUri ) 
	            												   throws TimeStampTokenGenerationException {
	    	try {
	    		tsRequest = tsRequest.evaluateMessageImprint(tsDigestInput);
	            tsResponse = gblTsService.processTsTequest(tsRequest);
	            timeStamp = tsResponse.getTimeStamp();
	            byte[] tsTokenEnc = timeStamp.getEncTimeStampToken();
	            TimeStampToken tsToken = new TimeStampToken(new CMSSignedData(tsTokenEnc));
	            return new TimeStampTokenRes(tsTokenEnc, tsToken.getTimeStampInfo().getGenTime());
	    	} catch(Exception e) {
            	throw new TimeStampTokenGenerationException(e.getMessage(), e);
            }
	    }

	    public void setTsRequest(TsRequestInfo tsRequest) {
	    	this.tsRequest = tsRequest;
	    }

		public TimeStampInfo getTimeStamp() {
			return timeStamp;
		}

		public TsResponseInfo getTsResponse() {
			return tsResponse;
		}
	}


    static class ExclusiveC14nForTimeStampsAlgorithmsProvider extends DefaultAlgorithmsProviderEx
    {
        @Override
        public Algorithm getCanonicalizationAlgorithmForTimeStampProperties()
        {
            return new ExclusiveCanonicalXMLWithoutComments();
        }

        @Override
        public Algorithm getCanonicalizationAlgorithmForSignature()
        {
            return new ExclusiveCanonicalXMLWithoutComments();
        }
    }


	
	public XMLSignatureService ( ) {
		try {		
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	    	dbf.setNamespaceAware(true);
	    	db = dbf.newDocumentBuilder();
	    	tf = TransformerFactory.newInstance();
	    } catch(Exception e) {
	    	throw new RuntimeException(e);
	    }
		if ( gblTsService == null ) {
			gblTsService = timeStampService;
		}
	}

	TransformerFactory tf;
	DocumentBuilder db;
	
	
	
	// -----
	// --- Pre-Sign phase
	// -
	
	/**
	 * Implementation of the preSign phase for the XML documents.  
	 * The certificate chain is then injected into the ExtKeyringDataProvider. 
	 * The signer will create the signatureId, used as base for the inner id generation. After that, the usage of 
	 * ExtXMLSignature allows it to obtain the digest of the xml envelope. XMLSignatureService receives now 
	 * digest and signatureId by the SignerExtBES and the signingTime by ExtSignaturePropertiesProvider
	 */
	public DigestSignature < SignCategory, 
							 SignDisposition.XML, 
							 VerifyResult, 		
							 XMLSignatureInfo > 		doPreSign (	ChainSignature < SignCategory, 
													 	 						 	 SignDisposition.XML, 
													 								 VerifyResult, 		
													 								 XMLSignatureInfo 		 >	chainSignature,
																	InputStream 								contentIs 		)
													 							 
													 										 throws SignatureException, IOException {
		
		DigestSignature < SignCategory, 
						  SignDisposition.XML, 
						  VerifyResult, 		
						  XMLSignatureInfo > digestSignature = null;
		try {
			Document doc = db.parse(contentIs);
	        Element root = doc.getDocumentElement();
	        DOMHelper.useIdAsXmlId(root);
	        
	        // External XAdES-BES Signer
	        ExtKeyringDataProvider extKdp = new ExtKeyringDataProvider();
	        ExtSignaturePropertiesProvider extSignPropertiesProvider = new ExtSignaturePropertiesProvider();
	        XadesSigningProfile signingProfile;
	        TsRequestInfo tsRequest = chainSignature.getTsRequest(); 
	        if ( tsRequest != null && StringUtils.isNotBlank(tsRequest.getTsUrl() )) {
	        	ExtTimeStampTokenProvider extTsTokenProvider = new ExtTimeStampTokenProvider();
	        	extTsTokenProvider.setTsRequest(tsRequest);
	        	signingProfile = new XadesExtTSigningProfile(extKdp)
			        	.withTimeStampTokenProvider(extTsTokenProvider)
		                .withAlgorithmsProviderEx(ExclusiveC14nForTimeStampsAlgorithmsProvider.class);
	        } else {
	        	signingProfile = new XadesExtBesSigningProfile(extKdp);
	        }
	        XadesSignerExt signer = (XadesSignerExt) signingProfile
					.withSignaturePropertiesProvider ( extSignPropertiesProvider )
							.newSigner();
	        
	        // Certificate chain injection
	        extKdp.setSigningCertificateChain ( 
	        		TemplateUtils.Conversion.arrayToList ( 
	        				chainSignature.getRawX509Certificates() ) );
	        
	        // Creation of the reference to the root element
	        String rootUri = DOMUtils.evalRootUri(root);
	        DataObjectDesc obj1 = new DataObjectReference(rootUri).withTransform( XPath2Filter.subtract("/descendant::ds:Signature") );
	        SignedDataObjects dataObjs = new SignedDataObjects(obj1);
	        dataObjs.withCommitmentType(new AllDataObjsCommitmentTypeProperty("http://uri.etsi.org/01903/v1.2.2#ProofOfOrigin", chainSignature.getReason()));
	        
	        // Digest evaluation
	        byte[] digest = signer.digest(dataObjs, root);
	        
	        // Inject the digest into the signature
	        DigestAlgorithm digestAlgorithm = chainSignature.getDigestAlgorithm();
	        digestSignature = chainSignature.toDigestSignature ( 
	        		DigestInfo.getInstance(digestAlgorithm, digest) );
	        
	        ((XMLSignatureInfo)digestSignature).setSignatureId(signer.getSignatureId());
	        digestSignature.setSigningTime ( extSignPropertiesProvider.getSigningTime() );
		} catch(Exception e) {
        	throw new SignatureException(e);
        }
        return digestSignature;
	}

	// -----
	// --- Post-Sign phase
	// -
	
	/**
	 * During the postSign phase, the service injects the signingTime into ExtSignaturePropertiesProvider
	 * and the digitalSignature and signatureId into ExtKeyringDataProvider. This will allow to have an
	 * The document generated by the signer is sent now back through the outputStream.
	 */
	@Override
	public FinalizedSignature < SignCategory, 
							 	SignDisposition.XML, 
							 	VerifyResult, 		
							 	XMLSignatureInfo > 		doPostSign (	SignedSignature	  <	SignCategory, 
													 	 						 		SignDisposition.XML, 
													 	 						 	 	VerifyResult, 		
													 	 						 	 	XMLSignatureInfo 		 >	signedSignature,
																	InputStream 									contentIs,
																	OutputStream 									detachedSignOs,
																	OutputStream 									embeddedSignOs,
																	OutputStream 									tsResultOs,
																	OutputStream 									markedSignOs 	)
																			
																			throws SignatureException, IOException 			{
		
		MarkedSignature    < SignCategory, 
						 	 SignDisposition.XML, 
						 	 VerifyResult, 		
						 	 XMLSignatureInfo > markedSignature = null;
		
		FinalizedSignature < SignCategory, 
						 	 SignDisposition.XML, 
						 	 VerifyResult, 		
						 	 XMLSignatureInfo > finalizedSignature = null;
		
		try {
			Document doc = db.parse(contentIs);
	        Element root = doc.getDocumentElement();
	        DOMHelper.useIdAsXmlId(root);
	        
	        TsRequestInfo tsRequest = signedSignature.getTsRequest(); 
	        boolean applyMark = tsRequest != null && StringUtils.isNotBlank(tsRequest.getTsUrl());
	        
	        // External XAdES-BES Signer
	        ExtKeyringDataProvider extKdp = new ExtKeyringDataProvider();
	        ExtSignaturePropertiesProvider extSignPropertiesProvider = new ExtSignaturePropertiesProvider();
	        ExtTimeStampTokenProvider extTsTokenProvider = null;
	        XadesSigningProfile signingProfile;
	        if ( applyMark ) {
	        	extTsTokenProvider = new ExtTimeStampTokenProvider();
	        	extTsTokenProvider.setTsRequest(tsRequest);
	        	signingProfile = new XadesExtTSigningProfile(extKdp)
			        	.withTimeStampTokenProvider(extTsTokenProvider)
		                .withAlgorithmsProviderEx(ExclusiveC14nForTimeStampsAlgorithmsProvider.class);
	        } else {
	        	signingProfile = new XadesExtBesSigningProfile(extKdp);
	        }
	        XadesSignerExt signer = (XadesSignerExt) signingProfile
					.withSignaturePropertiesProvider ( extSignPropertiesProvider )
							.newSigner();
	        
	        // Certificate chain injection
	        extKdp.setSigningCertificateChain ( 
	        		TemplateUtils.Conversion.arrayToList ( 
	        				signedSignature.getRawX509Certificates() ) );
	        
	        // SigningTime injection
	        extSignPropertiesProvider.setSigningTime ( signedSignature.getSigningTime() );
	        extSignPropertiesProvider.setLocation ( signedSignature.getLocation() );

	        // Creation of the reference to the root element
	        String rootUri = DOMUtils.evalRootUri(root);
	        DataObjectDesc obj1 = new DataObjectReference(rootUri).withTransform( XPath2Filter.subtract("/descendant::ds:Signature") );
	        SignedDataObjects dataObjs = new SignedDataObjects(obj1);
	        dataObjs.withCommitmentType(new AllDataObjsCommitmentTypeProperty("http://uri.etsi.org/01903/v1.2.2#ProofOfOrigin", signedSignature.getReason()));
	        
	        // Inject the digitalSignature into the signer
	        signer.setSignatureId(((XMLSignatureInfo)signedSignature).getSignatureId());
	        signer.setDigest ( signedSignature.getDigest().getFingerPrint() );
	        signer.setDigitalSignature ( signedSignature.getDigitalSignature() );
	        
	        // Generate the signed xml
	        XadesSignatureResult signResult = signer.sign(dataObjs, root, SignatureAppendingStrategies.AsLastChild);
	        XMLSignature xmlSignature = signResult.getSignature();
	        String expression = "*[local-name() = 'Signature']";
	        DOMUtils.replaceElement(doc.getDocumentElement(), expression, xmlSignature.getElement());
	        
	        // Finalize the signature and send the signed xml to the outputStream
	        OutputStream targetStream;
	        if ( applyMark ) {
	        	markedSignature = signedSignature.toMarkedSignature();
	        	markedSignature.appendTimeStamp(extTsTokenProvider.getTimeStamp(), SignDisposition.TimeStamp.ATTRIBUTE);
	        	finalizedSignature = markedSignature.finalizeSignature();
	        	targetStream = markedSignOs;
	        } else {
	        	finalizedSignature = signedSignature.finalizeSignature();
	        	targetStream = embeddedSignOs;
	        }
	    	tf.newTransformer().transform ( new DOMSource(doc), new StreamResult(targetStream) );
		} catch(Exception e) {
        	throw new SignatureException(e);
        }
        
        return finalizedSignature;
	}

	@Override
	public TimeStampInfo doApplyTimeStamp (
			TsRequestInfo tsRequest,
			InputStream contentIs,
			InputStream detachedSignIs,
			InputStream embeddedSignIs,
			OutputStream timestampOs,
			OutputStream markedSignOs ) 
					throws SignatureException,
							IOException {

		// TODO body method not implemented yet
		throw new UnsupportedOperationException ( "body method not implemented yet" );
	}

	@Override
	public VerifyInfo doVerify ( 
			InputStream contentIs,
			InputStream tsResponseIs,
			InputStream envelopeIs,
			OutputStream contentOs ) 
					throws 	CertificateException,
							SignatureException,
							IOException {

		XMLSignature xmlSignature;
		try {
			Document doc = db.parse(envelopeIs);
			xmlSignature = new XMLSignature(doc.getDocumentElement(), null);
		} catch(SAXException e) {
			throw new IOException("unable to parse the xml document", e);
		} catch(XMLSecurityException e) {
			throw new SignatureException("unable to load the signature", e);
		}
		
//		xmlSignature.checkSignatureValue(cert)
		// TODO body method not implemented yet
		throw new UnsupportedOperationException ( "body method not implemented yet" );
	}

	
	
	private static TimeStampService gblTsService;

}
